﻿using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reactive;
using System.Reflection;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Markup;
using Rxx.Properties;

/* This namespace is used instead of System.Windows, which is where EventTrigger and other triggers are located, because we 
 * want EventSubscription to be located in the same XAML namesapce as Subscription.  This makes it easier to use them within
 * the same UI because only a single namespace prefix must be defined; e.g., xmlns:rxx="clr-namespace:System.Windows.Reactive;assembly=Rxx"
 * Arguably, the Reactive namespace is more appropriate for EventSubscription anyway.
 */
namespace System.Windows.Reactive
{
	/// <summary>
	/// Represents a trigger that, in response to a routed event, invokes a handler that is either a <see cref="RoutedEventHandler"/> delegate, 
	/// an object that implements <see cref="ICommand"/> or an object that implements <see cref="IObserver{T}"/> where <strong>T</strong> 
	/// is a compatible <see cref="EventPattern{TEventArgs}"/>.
	/// </summary>
	/// <remarks>
	/// <alert type="warning">
	/// <see cref="EventSubscription"/> may only be used in a <see cref="FrameworkElement.Triggers"/> collection; otherwise, the specified 
	/// <see cref="Handler"/> will never be called.
	/// </alert>
	/// </remarks>
	[ContentProperty("")]
	public sealed class EventSubscription : EventTrigger
	{
		#region Public Properties
		/// <summary>
		/// Identifies the <see cref="Handler"/> dependency property.
		/// </summary>
		/// <value>The identifier for the <see cref="Handler"/> dependency property.</value>
		public static readonly DependencyProperty HandlerProperty = DependencyProperty.Register(
			"Handler",
			typeof(object),
			typeof(EventSubscription),
			new UIPropertyMetadata(null, HandlerChanged));

		/// <summary>
		/// Gets or sets the reference to a handler for the specified <see cref="RoutedEvent"/>.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The supported handler types are as follows:
		/// </para>
		/// <list type="bullet">
		/// <item>
		/// <see cref="IObserver{T}"/> where <strong>T</strong> is <see cref="EventPattern{TEventArgs}"/> and <strong>TEventArgs</strong> 
		/// is convertible contravariantly to the argument type of the specified <see cref="RoutedEvent"/>.  See below for more information.
		/// </item>
		/// <item>
		/// <see cref="Delegate"/> with a matching contravariant signature.  See below for more information.
		/// </item>
		/// <item>
		/// Any implementation of <see cref="ICommand"/>.  The command parameter will be an instance of 
		/// <see cref="EventPattern{TEventArgs}"/> where <strong>TEventArgs</strong> is the argument type 
		/// of the <see cref="RoutedEvent"/>.
		/// </item>
		/// </list>
		/// <para>
		/// Contravariance is supported for the parameters of a <see cref="RoutedEventHandler"/> delegate.  This means that the 
		/// delegate's <em>sender</em> parameter can be the exact type or any base class of the <see cref="FrameworkElement"/> that raises 
		/// the specified <see cref="RoutedEvent"/>.  Furthermore, the <em>EventArgs</em> parameter can be the actual argument type of the 
		/// specified <see cref="RoutedEvent"/> or any base class.
		/// </para>
		/// <para>
		/// Likewise, contravariance is supported for the generic type argument of an object that implements <see cref="IObserver{T}"/> where 
		/// <strong>T</strong> is <see cref="EventPattern{TEventArgs}"/> and <strong>TEventArgs</strong> can be the actual argument type of the 
		/// specified <see cref="RoutedEvent"/> or any base class to <see cref="EventArgs"/>.
		/// </para>
		/// </remarks>
		/// <value>Reference to the handler that is attached by this trigger.</value>
		public object Handler
		{
			get
			{
				return GetValue(HandlerProperty);
			}
			set
			{
				SetValue(HandlerProperty, value);
			}
		}

		/// <summary>
		/// Gets or sets a value indicating whether the <see cref="Handler"/> should still be invoked, even if the <see cref="RoutedEvent"/> 
		/// is marked handled in its event data.
		/// </summary>
		/// <value><see langword="True"/> if the <see cref="Handler"/> should still be invoked; otherwise, <see langword="false"/>.
		/// The default value is <see langword="false"/>.</value>
		[EditorBrowsable(EditorBrowsableState.Never)]
		public bool HandledEventsToo
		{
			get;
			set;
		}
		#endregion

		#region Private / Protected
		private static readonly DependencyProperty TargetProperty = DependencyProperty.Register(
			"Target",
			typeof(FrameworkElement),
			typeof(EventSubscription),
			new UIPropertyMetadata(null, TargetChanged));

		private const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
		private static readonly MethodInfo raiseEventMethod = typeof(EventSubscription).GetMethod("RaiseEvent", flags);
		private static readonly MethodInfo executeCommandMethod = typeof(EventSubscription).GetMethod("ExecuteCommand", flags);
		private static readonly MethodInfo notifyObserverMethod = typeof(EventSubscription).GetMethod("NotifyObserver", flags);

		private FrameworkElement target;
		private bool handlerSet;
		private Delegate handlerProxy;
		private Type eventPatternType;
		private Type observerPatternType;
		private Delegate handler;
		private ICommand command;
		private object observer;
		private MethodInfo onNext;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="EventSubscription" /> class.
		/// </summary>
		public EventSubscription()
		{
			SetTarget();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="EventSubscription" /> class with the specified <paramref name="routedEvent"/>.
		/// </summary>
		/// <param name="routedEvent">The <see cref="RoutedEvent"/> to be handled.</param>
		public EventSubscription(RoutedEvent routedEvent)
			: base(routedEvent)
		{
			Contract.Requires(routedEvent != null);

			SetTarget();
		}
		#endregion

		#region Methods
		[ContractInvariantMethod]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
		private void ObjectInvariant()
		{
			Contract.Invariant(handlerProxy == null || target != null);
		}

		[ContractVerification(false)]
		private static void TargetChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
		{
			Contract.Assume(obj != null);

			var trigger = (EventSubscription) obj;

			trigger.target = (FrameworkElement) e.NewValue;

			var routedEvent = trigger.RoutedEvent;

			Contract.Assume(routedEvent != null);

			var routedEventArgsType = GetEventArgsType(routedEvent);

			trigger.eventPatternType = typeof(EventPattern<>).MakeGenericType(routedEventArgsType);

			if (trigger.handlerSet)
			{
				trigger.ChangeHandler(trigger.Handler);
			}
		}

		private static Type GetEventArgsType(RoutedEvent routedEvent)
		{
			Contract.Requires(routedEvent != null);
			Contract.Ensures(Contract.Result<Type>() != null);

			var handlerType = routedEvent.HandlerType;

			Contract.Assume(handlerType != null);

			if (handlerType.IsGenericType)
			{
				var typeArguments = handlerType.GetGenericArguments();

				if (typeArguments.Length == 1)
				{
					var type = typeArguments[0];

					Contract.Assume(type != null);

					if (typeof(RoutedEventArgs).IsAssignableFrom(type))
					{
						return type;
					}
				}
			}
			else
			{
				var invoke = handlerType.GetMethod("Invoke");

				if (invoke != null)
				{
					var parameters = invoke.GetParameters();

					if (parameters.Length == 2)
					{
						Contract.Assume(parameters[1] != null);

						var type = parameters[1].ParameterType;

						if (typeof(RoutedEventArgs).IsAssignableFrom(type))
						{
							return type;
						}
					}
				}
			}

			throw new InvalidOperationException(Errors.CannotDetermineRoutedEventArgsType);
		}

		private static void HandlerChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
		{
			Contract.Requires(obj != null);

			var trigger = (EventSubscription) obj;

			trigger.ChangeHandler(e.NewValue);
		}

		private void SetTarget()
		{
			BindingOperations.SetBinding(
				this,
				TargetProperty,
				new Binding()
				{
					RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(FrameworkElement), 1)
				});
		}

		private void ChangeHandler(object value)
		{
			this.handlerSet = value != null;

			if (target == null)
			{
				/* In testing, Target was sometimes assigned before Handler, and sometimes it wasn't.
				 * Both cases are supported by the handlerSet field.
				 */
				return;
			}

			var routedEvent = RoutedEvent;

			if (routedEvent == null)
			{
				// The base class will eventually throw an exception since RoutedEvent is required.
				return;
			}

			Unsubscribe(routedEvent);

			Contract.Assume(eventPatternType != null);

			if (value != null)
			{
				handlerProxy = Convert(routedEvent, value);

				target.AddHandler(routedEvent, handlerProxy, HandledEventsToo);
			}
		}

		private void Unsubscribe(RoutedEvent routedEvent)
		{
			Contract.Requires(routedEvent != null);

			if (handlerProxy != null)
			{
				target.RemoveHandler(routedEvent, handlerProxy);

				handlerProxy = null;
				handler = null;
				command = null;
				observer = null;
				observerPatternType = null;
				onNext = null;
			}
		}

		private Delegate Convert(RoutedEvent routedEvent, object value)
		{
			Contract.Requires(routedEvent != null);
			Contract.Requires(value != null);
			Contract.Requires(eventPatternType != null);
			Contract.Ensures(Contract.Result<Delegate>() != null);

			Delegate proxy = null;

			handler = value as Delegate;

			if (handler != null)
			{
				var eventType = routedEvent.HandlerType;

				Contract.Assume(eventType != null);

				proxy = CreateDelegateProxy(eventType, handler);
			}
			else
			{
				command = value as ICommand;

				if (command != null)
				{
					proxy = Delegate.CreateDelegate(routedEvent.HandlerType, this, executeCommandMethod);
				}
				else
				{
					proxy = TryCreateObserverProxy(routedEvent, value);
				}
			}

			if (proxy != null)
			{
				return proxy;
			}

			throw new InvalidOperationException(Errors.EventSubscriptionIncompatibleHandler);
		}

		private Delegate CreateDelegateProxy(Type eventType, Delegate actualHandler)
		{
			Contract.Requires(eventType != null);
			Contract.Requires(actualHandler != null);
			Contract.Ensures(Contract.Result<Delegate>() != null);

			var invoke = eventType.GetMethod("Invoke");

			if (invoke != null)
			{
				var parameters = invoke.GetParameters();

				if (parameters.Length == 2)
				{
					Contract.Assume(parameters[0] != null);
					Contract.Assume(parameters[1] != null);

					var senderType = parameters[0].ParameterType;
					var eventArgsType = parameters[1].ParameterType;

					EnsureDelegateIsCompatible(senderType, eventArgsType, actualHandler);

					var d = Delegate.CreateDelegate(eventType, this, raiseEventMethod);

					Contract.Assume(d != null);

					return d;
				}
			}

			/* This should never occur because the EventTrigger base type ensures that a valid RoutedEvent is 
			 * specified, which ensures that the expected signature matches.
			 */
			throw new InvalidOperationException();
		}

		private static void EnsureDelegateIsCompatible(Type senderType, Type eventArgsType, Delegate handler)
		{
			Contract.Requires(senderType != null);
			Contract.Requires(eventArgsType != null);
			Contract.Requires(handler != null);

			var invoke = handler.GetType().GetMethod("Invoke");

			if (invoke != null)
			{
				var parameters = invoke.GetParameters();

				if (parameters.Length == 2)
				{
					Contract.Assume(parameters[0] != null);
					Contract.Assume(parameters[1] != null);

					var targetSenderType = parameters[0].ParameterType;

					if (targetSenderType.IsAssignableFrom(senderType))
					{
						var targetEventArgsType = parameters[1].ParameterType;

						if (targetEventArgsType.IsAssignableFrom(eventArgsType))
						{
							return;
						}
					}
				}
			}

			throw new InvalidOperationException(Errors.EventSubscriptionIncompatibleDelegate);
		}

		private Delegate TryCreateObserverProxy(RoutedEvent routedEvent, object value)
		{
			Contract.Requires(routedEvent != null);
			Contract.Requires(eventPatternType != null);
			Contract.Requires(value != null);

			var type = value.GetType();

			var observerType = type.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IObserver<>));

			if (observerType != null)
			{
				var arguments = observerType.GetGenericArguments();

				Contract.Assume(arguments.Length == 1);

				var observerArgument = arguments[0];

				Contract.Assume(observerArgument != null);

				if (eventPatternType.IsAssignableFrom(observerArgument))
				{
					observerPatternType = eventPatternType;

					goto Bind;
				}
				else if (observerArgument.IsGenericType && observerArgument.GetGenericTypeDefinition() == typeof(EventPattern<>))
				{
					arguments = observerArgument.GetGenericArguments();

					Contract.Assume(arguments.Length == 1);

					var targetEventArgument = arguments[0];

					Contract.Assume(targetEventArgument != null);

					arguments = eventPatternType.GetGenericArguments();

					Contract.Assume(arguments.Length == 1);

					var actualEventArgument = arguments[0];

					Contract.Assume(actualEventArgument != null);

					if (targetEventArgument.IsAssignableFrom(actualEventArgument))
					{
						observerPatternType = observerArgument;

						goto Bind;
					}
				}

				throw new InvalidOperationException(Errors.EventSubscriptionIncompatibleObserver);
			}

			return null;

		Bind:
			observer = value;

			var map = observer.GetType().GetInterfaceMap(observerType);

			Contract.Assume(map.TargetMethods != null);

			onNext = Array.Find(map.TargetMethods, method => method.Name == "OnNext");

			return Delegate.CreateDelegate(routedEvent.HandlerType, this, notifyObserverMethod);
		}

		private void RaiseEvent(object sender, RoutedEventArgs e)
		{
			Contract.Requires(handler != null);

			handler.DynamicInvoke(sender, e);
		}

		private void ExecuteCommand(object sender, RoutedEventArgs e)
		{
			Contract.Requires(command != null);
			Contract.Requires(eventPatternType != null);

			var @event = Activator.CreateInstance(eventPatternType, sender, e);

			if (command.CanExecute(@event))
			{
				command.Execute(@event);
			}
		}

		private void NotifyObserver(object sender, RoutedEventArgs e)
		{
			Contract.Requires(observer != null);
			Contract.Requires(observerPatternType != null);
			Contract.Requires(onNext != null);

			var @event = Activator.CreateInstance(observerPatternType, sender, e);

			onNext.Invoke(observer, new[] { @event });
		}
		#endregion
	}
}